#!/usr/bin/env python3
"""
超声心动图数据预处理脚本

支持多个数据集：
- BraTS2023: D:\Data\BraTS2023
- EchoNet-Dynamic: D:\Data\EchoNet-Dynamic
- CAMUS2019: D:\Data\CAMUS2019
- CardiacNet: D:\Data\CardiacNet
"""

import os
import sys
import argparse
import json
import cv2
import numpy as np
from pathlib import Path
from typing import List, Dict, Tuple, Optional, Union, Any
import shutil
from tqdm import tqdm
import yaml
import warnings

# 抑制SimpleITK/ITK版本警告（在导入之前设置）
os.environ['ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS'] = '1'
# 尝试通过环境变量抑制ITK警告（如果支持）
os.environ['ITK_SUPPRESS_WARNINGS'] = '1'
os.environ['ITK_NO_WARNINGS'] = '1'

# 抑制Python警告
warnings.filterwarnings('ignore', category=UserWarning)
warnings.filterwarnings('ignore', message='.*itk version.*')
warnings.filterwarnings('ignore', message='.*Possible incompatible factory load.*')

# 延迟导入SimpleITK：只在需要时导入，避免启动时的警告
# 使用全局变量来存储SimpleITK模块
_sitk_module = None
_sitk_imported = False

def _import_sitk_silently():
    """
    延迟导入SimpleITK，并尽可能抑制警告
    返回SimpleITK模块或None
    如果导入失败或出现警告导致问题，返回None并继续运行
    """
    global _sitk_module, _sitk_imported
    
    if _sitk_imported:
        return _sitk_module
    
    _sitk_imported = True
    
    from contextlib import redirect_stderr
    from io import StringIO
    import sys
    
    # 保存原始stderr
    original_stderr = sys.stderr
    original_stdout = sys.stdout
    
    try:
        # 创建空的StringIO来捕获所有输出
        null_stream = StringIO()
        
        # 重定向stderr和stdout
        sys.stderr = null_stream
        sys.stdout = null_stream
        
        # 导入SimpleITK（即使有警告也会继续）
        import SimpleITK as sitk
        
        # 恢复输出流
        sys.stderr = original_stderr
        sys.stdout = original_stdout
        
        _sitk_module = sitk
        return sitk
        
    except ImportError:
        sys.stderr = original_stderr
        sys.stdout = original_stdout
        _sitk_module = None
        return None
    except Exception as e:
        # 任何异常都捕获，确保程序继续运行
        sys.stderr = original_stderr
        sys.stdout = original_stdout
        _sitk_module = None
        return None

# 添加项目路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root / 'src'))

from utils.logger import get_logger
from utils.config import load_config
from data.loader import EchoVideoLoader
from data.preprocessor import EchoPreprocessor, ImageQualityFilter


class EchoDataPreprocessor:
    """超声心动图数据预处理器"""
    
    def __init__(self, 
                 config_path: Optional[Union[str, Path]] = None,
                 output_dir: Optional[Union[str, Path]] = None):
        """
        初始化预处理器
        
        Args:
            config_path: 配置文件路径
            output_dir: 输出目录
        """
        self.logger = get_logger("EchoDataPreprocessor")
        
        # 加载配置
        if config_path:
            self.config = load_config(config_path)
        else:
            self.config = self._get_default_config()
        
        # 设置输出目录
        self.output_dir = Path(output_dir) if output_dir else Path("data/processed/segmentation")
        self.output_dir.mkdir(parents=True, exist_ok=True)
        
        # 初始化预处理器
        self.preprocessor = EchoPreprocessor(
            image_size=tuple(self.config['data']['preprocessing']['image_size']),
            normalize=self.config['data']['preprocessing']['normalize'],
            mean=tuple(self.config['data']['preprocessing']['mean']),
            std=tuple(self.config['data']['preprocessing']['std'])
        )
        
        # 质量过滤器
        self.quality_filter = ImageQualityFilter(
            min_contrast=0.1,
            min_brightness=0.1,
            max_blur=0.8,
            min_sharpness=0.1
        )
        
        self.logger.info(f"预处理器初始化完成，输出目录: {self.output_dir}")
    
    def _convert_medical_image_to_png(self, 
                                     medical_file: Path, 
                                     output_png: Path,
                                     slice_index: Optional[int] = None) -> bool:
        """
        将医学图像格式（.mhd/.mha/.nii）转换为PNG格式
        
        Args:
            medical_file: 医学图像文件路径
            output_png: 输出PNG文件路径
            slice_index: 对于3D数据，选择哪个切片（None表示中间切片）
            
        Returns:
            是否转换成功
        """
        try:
            output_png.parent.mkdir(parents=True, exist_ok=True)
            
            # 检查文件扩展名
            ext = medical_file.suffix.lower()
            is_nii = (ext == '.nii' or 
                     (ext == '.gz' and len(medical_file.suffixes) > 1 and 
                      medical_file.suffixes[-2].lower() == '.nii') or
                     '.nii' in medical_file.name.lower())
            is_mhd = ext in ['.mhd', '.mha']
            
            image_data = None
            
            if is_nii:
                # 检查文件大小，跳过空文件
                try:
                    # 确保是文件而不是目录
                    if not medical_file.is_file():
                        self.logger.debug(f"跳过目录（非文件）: {medical_file.name}")
                        return False
                        
                    file_size = medical_file.stat().st_size
                    if file_size == 0:
                        self.logger.debug(f"跳过空文件: {medical_file.name}")
                        return False
                    # 如果文件太小（小于1KB），可能是损坏的
                    if file_size < 1024:
                        self.logger.debug(f"文件过小，可能损坏: {medical_file.name} ({file_size} bytes)")
                        return False
                except Exception as e:
                    self.logger.debug(f"无法检查文件大小 {medical_file.name}: {e}")
                    return False
                
                # 使用nibabel加载.nii文件
                try:
                    import nibabel as nib
                    nii_img = nib.load(str(medical_file))
                    image_data = nii_img.get_fdata()
                    
                    # 检查数据是否为空
                    if image_data is None or image_data.size == 0:
                        self.logger.debug(f"跳过空数据文件: {medical_file.name}")
                        return False
                except ImportError:
                    self.logger.error("nibabel未安装，无法读取.nii文件")
                    return False
                except Exception as e:
                    # 空文件错误改为debug级别，其他错误保持warning
                    error_msg = str(e).lower()
                    if 'empty file' in error_msg or 'file is empty' in error_msg:
                        self.logger.debug(f"跳过空文件: {medical_file.name}")
                    else:
                        self.logger.warning(f"读取.nii文件失败 {medical_file.name}: {e}")
                    return False
                    
            elif is_mhd:
                # 使用SimpleITK加载.mhd/.mha文件（延迟导入）
                sitk = _import_sitk_silently()
                if sitk is None:
                    self.logger.error("SimpleITK未安装，无法读取.mhd/.mha文件")
                    return False
                try:
                    image = sitk.ReadImage(str(medical_file))
                    image_data = sitk.GetArrayFromImage(image)
                except Exception as e:
                    self.logger.warning(f"读取.mhd/.mha文件失败 {medical_file}: {e}")
                    return False
            else:
                self.logger.warning(f"不支持的文件格式: {medical_file}")
                return False
            
            if image_data is None or image_data.size == 0:
                return False
            
            # 处理3D/4D数据
            if len(image_data.shape) == 3:
                # 3D数据：选择指定切片或中间切片
                if slice_index is None:
                    slice_index = image_data.shape[2] // 2
                image_data = image_data[:, :, slice_index]
            elif len(image_data.shape) == 4:
                # 4D数据：选择第一个时间点和指定切片
                if slice_index is None:
                    slice_index = image_data.shape[2] // 2
                image_data = image_data[:, :, slice_index, 0]
            
            # 归一化到0-255
            if image_data.max() > image_data.min():
                image_data = ((image_data - image_data.min()) / 
                             (image_data.max() - image_data.min()) * 255).astype(np.uint8)
            else:
                image_data = np.zeros_like(image_data, dtype=np.uint8)
            
            # 转换为3通道BGR格式（OpenCV格式）
            if len(image_data.shape) == 2:
                image_bgr = cv2.cvtColor(image_data, cv2.COLOR_GRAY2BGR)
            else:
                image_bgr = image_data
            
            # 保存为PNG
            cv2.imwrite(str(output_png), image_bgr)
            return True
            
        except Exception as e:
            self.logger.error(f"转换医学图像失败 {medical_file}: {e}")
            return False
    
    def _get_default_config(self) -> Dict[str, Any]:
        """获取默认配置"""
        return {
            'data': {
                'image_size': [1024, 1024],
                'preprocessing': {
                    'normalize': True,
                    'mean': [0.485, 0.456, 0.406],
                    'std': [0.229, 0.224, 0.225]
                },
                'split': {
                    'train_ratio': 0.7,
                    'val_ratio': 0.15,
                    'test_ratio': 0.15,
                    'random_seed': 42
                }
            }
        }
    
    def process_brats2023(self, data_dir: Union[str, Path]) -> Dict[str, Any]:
        """
        处理BraTS2023数据集
        
        Args:
            data_dir: BraTS2023数据目录
            
        Returns:
            处理结果统计
        """
        data_dir = Path(data_dir)
        self.logger.info(f"处理BraTS2023数据集: {data_dir}")
        
        if not data_dir.exists():
            self.logger.error(f"BraTS2023数据目录不存在: {data_dir}")
            return {}
        
        # BraTS2023结构检查（支持多种可能的目录结构）
        possible_dirs = [
            "ASNR-MICCAI-BraTS2023-GLI-Challenge-TrainingData",
            "BraTS2023_Training",
            "Training", 
            "train",
            "data"
        ]
        
        training_dir = None
        for dir_name in possible_dirs:
            test_dir = data_dir / dir_name
            if test_dir.exists():
                training_dir = test_dir
                self.logger.info(f"找到BraTS2023训练目录: {training_dir}")
                break
        
        if training_dir is None:
            # 检查是否直接在根目录下有患者文件夹
            patient_dirs = [d for d in data_dir.iterdir() if d.is_dir() and d.name.startswith('BraTS')]
            if patient_dirs:
                training_dir = data_dir
                self.logger.info(f"BraTS2023患者目录直接在根目录: {training_dir}")
            else:
                # 列出实际目录内容以便调试
                actual_dirs = [d.name for d in data_dir.iterdir() if d.is_dir()][:10]
                self.logger.error(f"BraTS2023训练目录不存在")
                self.logger.error(f"  尝试的目录: {possible_dirs}")
                self.logger.error(f"  实际目录内容: {actual_dirs}")
                return {}
        
        processed_samples = []
        total_samples = 0
        valid_samples = 0
        
        # 处理每个患者
        for patient_dir in tqdm(training_dir.iterdir(), desc="处理BraTS2023"):
            if not patient_dir.is_dir():
                continue
            
                                                                                 # 跳过与训练目录同名的子目录（避免重复路径问题）
            if patient_dir.name == training_dir.name:
                self.logger.debug(f"跳过同名目录: {patient_dir}")
                continue
            
            # 跳过非患者目录（如README、LICENSE等）
            if patient_dir.name.lower() in ['readme', 'license', 'docs', 'documentation']:
                continue
            
            total_samples += 1
            
            try:
                # 查找T1CE图像（支持多种命名格式，大小写不敏感）
                t1ce_patterns = [
                    "*t1ce.nii.gz", "*T1CE.nii.gz", "*t1ce.nii", "*T1CE.nii",
                    "*t1ce.nii*", "*T1CE.nii*", "*t1ce*", "*T1CE*"
                ]
                t1ce_files = []
                for pattern in t1ce_patterns:
                    t1ce_files = list(patient_dir.glob(pattern))
                    if t1ce_files:
                        break
                
                # 如果还没找到，尝试递归查找
                if not t1ce_files:
                    for pattern in ["**/*t1ce*.nii*", "**/*T1CE*.nii*"]:
                        t1ce_files = list(patient_dir.glob(pattern))
                        if t1ce_files:
                            break
                
                if not t1ce_files:
                    self.logger.warning(f"未找到T1CE文件: {patient_dir} (已尝试的目录内容: {[f.name for f in patient_dir.iterdir()][:5]})")
                    continue
                
                # 查找分割掩码（支持多种命名格式）
                seg_patterns = [
                    "*seg.nii.gz", "*SEG.nii.gz", "*seg.nii", "*SEG.nii",
                    "*seg.nii*", "*SEG.nii*", "*seg*", "*SEG*", "*label*", "*Label*"
                ]
                seg_files = []
                for pattern in seg_patterns:
                    seg_files = list(patient_dir.glob(pattern))
                    if seg_files:
                        break
                
                # 如果还没找到，尝试递归查找
                if not seg_files:
                    for pattern in ["**/*seg*.nii*", "**/*SEG*.nii*", "**/*label*.nii*"]:
                        seg_files = list(patient_dir.glob(pattern))
                        if seg_files:
                            break
                
                if not seg_files:
                    self.logger.warning(f"未找到分割文件: {patient_dir}")
                    continue
                
                # 处理图像
                result = self._process_brats_patient(patient_dir, t1ce_files[0], seg_files[0])
                if result:
                    processed_samples.append(result)
                    valid_samples += 1
                    
            except Exception as e:
                self.logger.error(f"处理患者失败 {patient_dir}: {e}", exc_info=True)
                continue
        
        self.logger.info(f"BraTS2023处理完成: {valid_samples}/{total_samples} 个样本")
        return {
            'dataset': 'BraTS2023',
            'total_samples': total_samples,
            'valid_samples': valid_samples,
            'samples': processed_samples
        }
    
    def _process_brats_patient(self, 
                              patient_dir: Path, 
                              t1ce_file: Path, 
                              seg_file: Path) -> Optional[Dict[str, Any]]:
        """处理单个BraTS患者"""
        try:
            import nibabel as nib
            
            # 加载图像和掩码
            t1ce_img = nib.load(t1ce_file)
            seg_img = nib.load(seg_file)
            
            t1ce_data = t1ce_img.get_fdata()
            seg_data = seg_img.get_fdata()
            
            # 选择中间切片
            middle_slice = t1ce_data.shape[2] // 2
            t1ce_slice = t1ce_data[:, :, middle_slice]
            seg_slice = seg_data[:, :, middle_slice]
            
            # 归一化到0-255
            t1ce_slice = ((t1ce_slice - t1ce_slice.min()) / 
                         (t1ce_slice.max() - t1ce_slice.min()) * 255).astype(np.uint8)
            
            # 转换为3通道
            t1ce_slice = cv2.cvtColor(t1ce_slice, cv2.COLOR_GRAY2BGR)
            
            # 处理掩码（只保留肿瘤区域，标签1和4）
            seg_slice = ((seg_slice == 1) | (seg_slice == 4)).astype(np.uint8) * 255
            
            # 质量检查
            if not self.quality_filter.is_good_quality(t1ce_slice):
                return None
            
            # 预处理
            processed_image = self.preprocessor.preprocess_image(t1ce_slice)
            processed_mask = cv2.resize(seg_slice, (self.config['data']['preprocessing']['image_size'][1], 
                                                  self.config['data']['preprocessing']['image_size'][0]))
            
            # 保存处理后的数据
            patient_id = patient_dir.name
            output_patient_dir = self.output_dir / "brats2023" / patient_id
            output_patient_dir.mkdir(parents=True, exist_ok=True)
            
            # 保存图像和掩码
            cv2.imwrite(str(output_patient_dir / "image.png"), t1ce_slice)
            cv2.imwrite(str(output_patient_dir / "mask.png"), seg_slice)
            np.save(str(output_patient_dir / "processed_image.npy"), processed_image)
            np.save(str(output_patient_dir / "processed_mask.npy"), processed_mask)
            
            return {
                'id': patient_id,
                'type': 'brats2023',
                'image_path': str(output_patient_dir / "image.png"),
                'mask_path': str(output_patient_dir / "mask.png"),
                'processed_image_path': str(output_patient_dir / "processed_image.npy"),
                'processed_mask_path': str(output_patient_dir / "processed_mask.npy")
            }
            
        except Exception as e:
            self.logger.error(f"处理BraTS患者失败 {patient_dir}: {e}")
            return None
    
    def process_echonet_dynamic(self, data_dir: Union[str, Path], max_videos: int = 100) -> Dict[str, Any]:
        """
        处理EchoNet-Dynamic数据集（优化版）
        
        Args:
            data_dir: EchoNet-Dynamic数据目录
            max_videos: 最大处理视频数量（用于快速测试）
            
        Returns:
            处理结果统计
        """
        data_dir = Path(data_dir)
        self.logger.info(f"处理EchoNet-Dynamic数据集: {data_dir}")
        
        if not data_dir.exists():
            self.logger.error(f"EchoNet-Dynamic数据目录不存在: {data_dir}")
            return {}
        
        # EchoNet-Dynamic结构: FileList.csv, Videos/, VolumeTracings.csv
        videos_dir = data_dir / "Videos"
        if not videos_dir.exists():
            self.logger.error(f"EchoNet-Dynamic视频目录不存在: {videos_dir}")
            return {}
        
        # 加载文件列表
        filelist_path = data_dir / "FileList.csv"
        if not filelist_path.exists():
            self.logger.error(f"FileList.csv不存在: {filelist_path}")
            return {}
        
        import pandas as pd
        filelist = pd.read_csv(filelist_path)
        
        # 限制处理数量（快速模式）
        if max_videos > 0:
            filelist = filelist.head(max_videos)
            self.logger.info(f"快速模式：只处理前 {max_videos} 个视频")
        
        processed_samples = []
        total_samples = 0
        valid_samples = 0
        
        # 处理每个视频
        for _, row in tqdm(filelist.iterrows(), desc="处理EchoNet-Dynamic", total=len(filelist)):
            video_file = videos_dir / f"{row['FileName']}.avi"
            if not video_file.exists():
                continue
            
            total_samples += 1
            
            try:
                result = self._process_echonet_video(video_file, row)
                if result:
                    processed_samples.append(result)
                    valid_samples += 1
                    
            except Exception as e:
                self.logger.error(f"处理视频失败 {video_file}: {e}")
                continue
        
        self.logger.info(f"EchoNet-Dynamic处理完成: {valid_samples}/{total_samples} 个样本")
        return {
            'dataset': 'EchoNet-Dynamic',
            'total_samples': total_samples,
            'valid_samples': valid_samples,
            'samples': processed_samples
        }
    
    def _detect_similar_frames(self, frames: List[np.ndarray], threshold: float = 0.95) -> List[int]:
        """
        检测相似帧的位置
        
        Args:
            frames: 帧列表
            threshold: 相似度阈值
            
        Returns:
            相似帧的索引列表
        """
        if len(frames) < 2:
            return []
        
        similar_groups = []
        processed = set()
        
        for i in range(len(frames)):
            if i in processed:
                continue
                
            similar_frames = [i]
            processed.add(i)
            
            # 将第一帧转换为灰度图用于比较
            gray1 = cv2.cvtColor(frames[i], cv2.COLOR_BGR2GRAY)
            
            for j in range(i + 1, len(frames)):
                if j in processed:
                    continue
                    
                gray2 = cv2.cvtColor(frames[j], cv2.COLOR_BGR2GRAY)
                
                # 计算结构相似性
                try:
                    ssim_score = cv2.matchTemplate(gray1, gray2, cv2.TM_CCOEFF_NORMED)[0][0]
                    if ssim_score > threshold:
                        similar_frames.append(j)
                        processed.add(j)
                except:
                    # 如果尺寸不匹配，跳过
                    continue
            
            if len(similar_frames) > 1:
                similar_groups.append(similar_frames)
        
        return similar_groups
    
    def _extract_key_frames(self, frames: List[np.ndarray], max_frames: int = 20) -> Tuple[List[np.ndarray], List[int]]:
        """
        提取关键帧
        
        Args:
            frames: 原始帧列表
            max_frames: 最大帧数
            
        Returns:
            (关键帧列表, 原始索引列表)
        """
        if len(frames) <= max_frames:
            return frames, list(range(len(frames)))
        
        # 检测相似帧
        similar_groups = self._detect_similar_frames(frames, threshold=0.9)
        
        # 选择关键帧
        key_frames = []
        key_indices = []
        
        # 首先添加每个相似组的代表帧
        for group in similar_groups:
            # 选择组中质量最好的帧
            best_idx = group[0]
            best_quality = 0
            
            for idx in group:
                if idx < len(frames):
                    quality = self.quality_filter.calculate_sharpness(frames[idx])
                    if quality > best_quality:
                        best_quality = quality
                        best_idx = idx
            
            key_frames.append(frames[best_idx])
            key_indices.append(best_idx)
        
        # 添加未分组的帧
        all_grouped = set()
        for group in similar_groups:
            all_grouped.update(group)
        
        for i, frame in enumerate(frames):
            if i not in all_grouped:
                key_frames.append(frame)
                key_indices.append(i)
        
        # 如果还是太多，均匀采样
        if len(key_frames) > max_frames:
            step = len(key_frames) // max_frames
            key_frames = key_frames[::step][:max_frames]
            key_indices = key_indices[::step][:max_frames]
        
        return key_frames, key_indices

    def _process_echonet_video(self, video_file: Path, metadata: Any) -> Optional[Dict[str, Any]]:
        """处理EchoNet-Dynamic视频（优化版）"""
        try:
            # 加载视频
            cap = cv2.VideoCapture(str(video_file))
            if not cap.isOpened():
                return None
            
            frames = []
            frame_count = 0
            
            # 读取所有帧（不调整尺寸，保持原始质量）
            while True:
                ret, frame = cap.read()
                if not ret:
                    break
                frames.append(frame)
                frame_count += 1
            
            cap.release()
            
            if not frames:
                return None
            
            # 快速质量检查（只检查前几帧）
            sample_frames = frames[:min(5, len(frames))]
            quality_scores = [self.quality_filter.calculate_sharpness(f) for f in sample_frames]
            avg_quality = np.mean(quality_scores)
            
            if avg_quality < 0.05:  # 降低质量阈值
                self.logger.warning(f"视频质量过低，跳过: {video_file.name}")
                return None
            
            # 提取关键帧（最多20帧）
            key_frames, key_indices = self._extract_key_frames(frames, max_frames=20)
            
            # 调整关键帧尺寸
            resized_frames = []
            for frame in key_frames:
                resized_frame = cv2.resize(frame, (self.config['data']['preprocessing']['image_size'][1], 
                                                 self.config['data']['preprocessing']['image_size'][0]))
                resized_frames.append(resized_frame)
            
            # 保存处理后的数据
            video_id = video_file.stem
            output_video_dir = self.output_dir / "echonet_dynamic" / video_id
            output_video_dir.mkdir(parents=True, exist_ok=True)
            
            # 只保存关键帧（不保存所有帧）
            for i, frame in enumerate(resized_frames):
                cv2.imwrite(str(output_video_dir / f"keyframe_{i:03d}.png"), frame)
            
            # 保存关键帧索引和相似帧信息
            similar_groups = self._detect_similar_frames(resized_frames, threshold=0.8)
            
            # 保存元数据
            video_metadata = {
                'original_frames': len(frames),
                'key_frames': len(key_frames),
                'key_indices': key_indices,
                'similar_groups': similar_groups,
                'quality_score': float(avg_quality),
                'video_info': {
                    'fps': cap.get(cv2.CAP_PROP_FPS) if cap.isOpened() else 0,
                    'duration': len(frames) / (cap.get(cv2.CAP_PROP_FPS) if cap.isOpened() else 1)
                }
            }
            
            # 保存元数据
            with open(str(output_video_dir / "metadata.json"), 'w', encoding='utf-8') as f:
                json.dump(video_metadata, f, indent=2, ensure_ascii=False)
            
            return {
                'id': video_id,
                'type': 'echonet_dynamic',
                'video_path': str(video_file),
                'frames_dir': str(output_video_dir),
                'num_frames': len(key_frames),
                'original_frames': len(frames),
                'quality_score': float(avg_quality),
                'similar_groups': similar_groups,
                'metadata': metadata.to_dict() if hasattr(metadata, 'to_dict') else str(metadata)
            }
            
        except Exception as e:
            self.logger.error(f"处理EchoNet-Dynamic视频失败 {video_file}: {e}")
            return None
    
    def _diagnose_cardiacnet_structure(self, data_dir: Path) -> Dict[str, Any]:
        """
        诊断CardiacNet数据集结构，找出问题文件
        
        Returns:
            诊断信息
        """
        diagnosis = {
            'total_nii_files': 0,
            'empty_files': [],
            'small_files': [],
            'valid_files': [],
            'directory_structure': {},
            'file_size_stats': {}
        }
        
        # 查找所有.nii文件（包括可能是目录的情况）
        nii_files = []
        nii_paths = list(data_dir.rglob('*.nii'))
        
        for path in nii_paths:
            # 检查是否是文件
            if path.is_file():
                nii_files.append(path)
            # 如果路径名以.nii结尾但实际是目录，检查目录内的文件
            elif path.is_dir():
                # 在目录内查找实际的.nii文件（包括.nii.gz）
                inner_nii = list(path.rglob('*.nii'))
                inner_gz = list(path.rglob('*.nii.gz'))
                # 只添加文件，不添加目录
                inner_nii = [f for f in inner_nii if f.is_file()]
                inner_gz = [f for f in inner_gz if f.is_file()]
                if inner_nii or inner_gz:
                    nii_files.extend(inner_nii)
                    nii_files.extend(inner_gz)
        
        diagnosis['total_nii_files'] = len(nii_files)
        
        # 分析文件大小
        file_sizes = []
        for f in nii_files:
            try:
                # 确保是文件而不是目录
                if not f.is_file():
                    continue
                    
                size = f.stat().st_size
                file_sizes.append(size)
                
                if size == 0:
                    diagnosis['empty_files'].append(str(f))
                elif size < 1024:
                    diagnosis['small_files'].append((str(f), size))
                else:
                    diagnosis['valid_files'].append((str(f), size))
            except Exception as e:
                self.logger.debug(f"无法分析文件 {f}: {e}")
                pass
        
        if file_sizes:
            diagnosis['file_size_stats'] = {
                'min': min(file_sizes),
                'max': max(file_sizes),
                'mean': sum(file_sizes) / len(file_sizes),
                'median': sorted(file_sizes)[len(file_sizes) // 2]
            }
        
        # 分析目录结构
        subdirs = [d for d in data_dir.iterdir() if d.is_dir()]
        for subdir in subdirs[:5]:  # 只检查前5个子目录
            files_in_subdir = list(subdir.rglob('*.nii'))
            diagnosis['directory_structure'][subdir.name] = {
                'total_files': len(files_in_subdir),
                'empty_count': sum(1 for f in files_in_subdir if f.stat().st_size == 0)
            }
        
        return diagnosis
    
    def process_cardiacnet(self, data_dir: Union[str, Path], max_images: int = 200) -> Dict[str, Any]:
        """
        处理CardiacNet数据集（优化版）
        
        Args:
            data_dir: CardiacNet数据目录
            max_images: 最大处理图像数量（用于快速测试）
            
        Returns:
            处理结果统计
        """
        data_dir = Path(data_dir)
        self.logger.info(f"处理CardiacNet数据集: {data_dir}")
        
        if not data_dir.exists():
            self.logger.error(f"CardiacNet数据目录不存在: {data_dir}")
            return {}
        
        # 诊断数据集结构
        self.logger.info("正在诊断CardiacNet数据集结构...")
        diagnosis = self._diagnose_cardiacnet_structure(data_dir)
        
        self.logger.info(f"诊断结果:")
        self.logger.info(f"  总.nii文件数: {diagnosis['total_nii_files']}")
        self.logger.info(f"  空文件数: {len(diagnosis['empty_files'])}")
        self.logger.info(f"  过小文件数: {len(diagnosis['small_files'])}")
        self.logger.info(f"  有效文件数: {len(diagnosis['valid_files'])}")
        
        if diagnosis['file_size_stats']:
            stats = diagnosis['file_size_stats']
            self.logger.info(f"  文件大小统计: 最小={stats['min']}B, 最大={stats['max']}B, "
                           f"平均={stats['mean']:.0f}B, 中位数={stats['median']}B")
        
        # 如果空文件太多，尝试查找替代数据源或修复
        if len(diagnosis['empty_files']) > len(diagnosis['valid_files']):
            self.logger.warning(f"发现大量空文件（{len(diagnosis['empty_files'])}个），"
                              f"尝试查找其他格式的数据文件...")
            
            # 检查是否有其他格式的文件（如.mat, .h5, .npz等）
            alt_extensions = ['*.mat', '*.h5', '*.hdf5', '*.npz', '*.npy']
            alt_files = []
            for ext in alt_extensions:
                alt_files.extend(data_dir.rglob(ext))
            
            if alt_files:
                self.logger.info(f"找到 {len(alt_files)} 个其他格式的数据文件")
                # 可以在这里添加处理其他格式的逻辑
            
            # 尝试查找是否有对应的有效文件（检查文件名模式）
            self.logger.info("尝试查找可能的有效数据文件...")
            # 检查是否有对应的.nii.gz文件（压缩版本）
            nii_gz_files = list(data_dir.rglob('*.nii.gz'))
            if nii_gz_files:
                valid_gz = [f for f in nii_gz_files if f.is_file() and f.stat().st_size > 1024]
                if valid_gz:
                    self.logger.info(f"找到 {len(valid_gz)} 个有效的.nii.gz文件，将使用这些文件")
                    # 将这些文件添加到处理列表（需要修改后续逻辑）
        
        # 查找所有图像文件（支持更多格式和目录结构）
        # 策略：处理.nii目录结构，查找目录内的实际文件
        image_files = []
        
        # 首先查找.nii.gz文件（压缩版本，通常更完整）
        nii_gz_files = list(data_dir.rglob('*.nii.gz'))
        if nii_gz_files:
            valid_gz = [f for f in nii_gz_files if f.is_file() and f.stat().st_size > 1024]
            if valid_gz:
                self.logger.info(f"找到 {len(valid_gz)} 个有效的.nii.gz文件（压缩版本），优先使用")
                image_files.extend(valid_gz)
        
        # 查找.nii文件（包括处理.nii目录的情况）
        if not image_files or len(image_files) < 100:  # 如果.nii.gz文件不够，继续查找.nii
            # 查找所有.nii路径（可能是文件或目录）
            nii_paths = list(data_dir.rglob('*.nii'))
            nii_file_list = []
            
            for path in nii_paths:
                if path.is_file():
                    # 只添加非空文件
                    try:
                        if path.stat().st_size > 1024:
                            nii_file_list.append(path)
                    except Exception:
                        pass
                elif path.is_dir():
                    # .nii目录：查找目录内的实际文件
                    inner_files = list(path.rglob('*.nii'))
                    inner_gz = list(path.rglob('*.nii.gz'))
                    # 只添加文件，过滤掉目录
                    inner_files = [f for f in inner_files if f.is_file()]
                    inner_gz = [f for f in inner_gz if f.is_file()]
                    # 只添加非空文件
                    for f in inner_files + inner_gz:
                        try:
                            if f.stat().st_size > 1024:
                                nii_file_list.append(f)
                        except Exception:
                            pass
            
            if nii_file_list:
                self.logger.info(f"找到 {len(nii_file_list)} 个有效的.nii文件（包括从.nii目录中提取的）")
                image_files.extend(nii_file_list)
        
        # 如果还没有找到足够的文件，查找其他格式
        if not image_files:
            extensions = ['*.png', '*.jpg', '*.jpeg', '*.bmp', '*.tiff', '*.tif', 
                         '*.PNG', '*.JPG', '*.JPEG', '*.BMP', '*.TIFF', '*.TIF',
                         '*.mhd', '*.mha']
            
            # 1. 首先在根目录查找
            for ext in extensions:
                found = list(data_dir.glob(ext))
                image_files.extend(found)
            
            if image_files:
                self.logger.info(f"在根目录找到 {len(image_files)} 个图像文件")
            
            # 2. 如果根目录没有，在子目录中递归查找
            if not image_files:
                self.logger.info("根目录未找到图像，尝试递归查找...")
                for ext in extensions:
                    found = list(data_dir.rglob(ext))
                    image_files.extend(found)
                    if found:
                        self.logger.debug(f"使用模式 {ext} 找到 {len(found)} 个文件")
        
        # 3. 如果还是没有，尝试常见的子目录
        if not image_files:
            self.logger.info("尝试在常见子目录中查找...")
            common_dirs = ['images', 'data', 'train', 'test', 'val', 'validation', 
                          'samples', 'dataset', 'raw', 'processed', 'cardiacnet']
            for subdir in common_dirs:
                subdir_path = data_dir / subdir
                if subdir_path.exists():
                    self.logger.info(f"检查子目录: {subdir_path}")
                    for ext in extensions:
                        found = list(subdir_path.rglob(ext))
                        image_files.extend(found)
                        if found:
                            self.logger.info(f"在子目录 {subdir} 中找到 {len(found)} 个图像文件")
                    if image_files:
                        break
        
        # 4. 如果仍然没找到，列出目录内容以便调试
        if not image_files:
            self.logger.warning(f"未找到图像文件，目录内容: {[f.name for f in data_dir.iterdir()][:10]}")
            # 尝试查找所有文件（不限制扩展名）
            all_files = list(data_dir.rglob('*'))
            file_types = {}
            for f in all_files[:100]:  # 只检查前100个文件
                if f.is_file():
                    ext = f.suffix.lower()
                    file_types[ext] = file_types.get(ext, 0) + 1
            if file_types:
                self.logger.info(f"发现的文件类型统计: {dict(sorted(file_types.items(), key=lambda x: x[1], reverse=True)[:10])}")
        
        # 去重并过滤掩码文件和空文件
        image_files = list(set(image_files))
        
        # 过滤掉可能的掩码文件（通常命名为*_label*, *_mask*, *_gt*等）
        exclude_keywords = ['label', 'mask', '_gt', 'segmentation', 'seg', 'groundtruth']
        filtered_files = []
        empty_files = []
        small_files = []
        
        for f in image_files:
            fname_lower = f.name.lower()
            # 跳过掩码文件
            if any(keyword in fname_lower for keyword in exclude_keywords):
                continue
            
            # 检查文件大小，跳过空文件和小文件
            try:
                file_size = f.stat().st_size
                if file_size == 0:
                    empty_files.append(f)
                    continue
                # 对于.nii文件，如果小于1KB可能是损坏的
                if '.nii' in f.name.lower() and file_size < 1024:
                    small_files.append(f)
                    continue
            except Exception:
                pass  # 如果无法获取文件大小，继续处理
            
            filtered_files.append(f)
        
        # 记录过滤统计
        if len(filtered_files) < len(image_files):
            excluded = len(image_files) - len(filtered_files)
            self.logger.info(f"过滤掉 {excluded} 个文件（掩码/空文件/损坏文件）")
            if empty_files:
                self.logger.debug(f"  其中空文件: {len(empty_files)} 个")
            if small_files:
                self.logger.debug(f"  其中过小文件: {len(small_files)} 个")
        
        image_files = filtered_files
        self.logger.info(f"CardiacNet找到 {len(image_files)} 个有效图像文件")
        
        # 限制处理数量（快速模式）
        if max_images > 0 and len(image_files) > max_images:
            image_files = image_files[:max_images]
            self.logger.info(f"快速模式：只处理前 {max_images} 个图像")
        
        processed_samples = []
        total_samples = len(image_files)
        valid_samples = 0
        failed_count = 0
        quality_failed = 0
        conversion_failed = 0
        other_failed = 0
        
        # 为CardiacNet使用更宽松的质量过滤器（医学图像可能对比度/亮度较低）
        cardiacnet_quality_filter = ImageQualityFilter(
            min_contrast=0.05,  # 降低对比度要求（医学图像可能对比度较低）
            min_brightness=0.05,  # 降低亮度要求
            max_blur=1.0,  # 放宽模糊度要求
            min_sharpness=0.05  # 降低锐度要求
        )
        
        # 临时替换质量过滤器和预处理器（使用不输出警告的版本）
        original_filter = self.quality_filter
        original_preprocessor = self.preprocessor
        
        # 为CardiacNet创建特殊的预处理器（不输出质量警告）
        from data.preprocessor import EchoPreprocessor
        cardiacnet_preprocessor = EchoPreprocessor(
            image_size=tuple(self.config['data']['preprocessing']['image_size']),
            normalize=self.config['data']['preprocessing']['normalize'],
            mean=tuple(self.config['data']['preprocessing']['mean']),
            std=tuple(self.config['data']['preprocessing']['std']),
            quality_filter=cardiacnet_quality_filter
        )
        # 临时禁用质量警告（通过设置logger级别）
        import logging
        # 获取logger的当前级别（兼容不同的logger实现）
        # 直接访问handler的级别，或者使用setLevel但不保存原始级别
        try:
            # 尝试设置级别，如果失败就忽略
            cardiacnet_preprocessor.logger.setLevel(logging.ERROR)  # 只显示ERROR，不显示WARNING
            original_level = logging.WARNING  # 默认恢复值
        except Exception:
            original_level = logging.WARNING
        
        self.quality_filter = cardiacnet_quality_filter
        self.preprocessor = cardiacnet_preprocessor
        
        # 处理每个图像
        for img_file in tqdm(image_files, desc="处理CardiacNet"):
            try:
                result = self._process_cardiacnet_image(img_file)
                if result:
                    processed_samples.append(result)
                    valid_samples += 1
                else:
                    failed_count += 1
                    # 尝试诊断失败原因（通过检查转换后的文件）
                    cache_dir = self.output_dir / "cache" / "cardiacnet"
                    cached_png = cache_dir / f"{img_file.stem}.png"
                    if cached_png.exists():
                        quality_failed += 1
                    else:
                        conversion_failed += 1
                    
            except Exception as e:
                self.logger.warning(f"处理图像失败 {img_file.name}: {e}")
                failed_count += 1
                other_failed += 1
                continue
        
        # 恢复原始质量过滤器和预处理器
        self.quality_filter = original_filter
        self.preprocessor = original_preprocessor
        try:
            cardiacnet_preprocessor.logger.setLevel(original_level)  # 恢复日志级别
        except Exception:
            pass  # 如果恢复失败，忽略
        
        if failed_count > 0:
            self.logger.info(f"CardiacNet处理统计: 成功 {valid_samples} 个, 失败 {failed_count} 个")
            if quality_failed > 0:
                self.logger.info(f"  质量检查失败: {quality_failed} 个 (已使用宽松标准)")
            if conversion_failed > 0:
                self.logger.info(f"  文件转换失败: {conversion_failed} 个")
            if other_failed > 0:
                self.logger.info(f"  其他错误: {other_failed} 个")
        
        self.logger.info(f"CardiacNet处理完成: {valid_samples}/{total_samples} 个样本")
        return {
            'dataset': 'CardiacNet',
            'total_samples': total_samples,
            'valid_samples': valid_samples,
            'samples': processed_samples
        }
    
    def _process_cardiacnet_image(self, img_file: Path) -> Optional[Dict[str, Any]]:
        """处理CardiacNet图像（支持.nii格式，自动转换为PNG）"""
        try:
            image = None
            
            # 根据文件扩展名选择加载方式
            # 检查是否为.nii或.nii.gz文件
            is_nii_file = (img_file.suffix == '.nii' or 
                          img_file.suffix == '.gz' and len(img_file.suffixes) > 1 and img_file.suffixes[-2] == '.nii' or
                          '.nii' in str(img_file.name).lower())
            
            if is_nii_file:
                # 检查文件大小，跳过空文件
                try:
                    file_size = img_file.stat().st_size
                    if file_size == 0:
                        self.logger.debug(f"跳过空文件: {img_file.name}")
                        return None
                    # 如果文件太小（小于1KB），可能是损坏的
                    if file_size < 1024:
                        self.logger.debug(f"文件过小，可能损坏: {img_file.name} ({file_size} bytes)")
                        return None
                except Exception:
                    pass  # 如果无法获取文件大小，继续尝试读取
                
                # 创建转换缓存目录
                cache_dir = self.output_dir / "cache" / "cardiacnet"
                cache_dir.mkdir(parents=True, exist_ok=True)
                
                # 检查是否有已转换的PNG文件
                cached_png = cache_dir / f"{img_file.stem}.png"
                if cached_png.exists():
                    # 直接加载PNG
                    image = cv2.imread(str(cached_png))
                    if image is None:
                        self.logger.debug(f"无法加载缓存的PNG文件: {cached_png.name}")
                        return None
                else:
                    # 转换.nii文件为PNG
                    if not self._convert_medical_image_to_png(img_file, cached_png):
                        # 转换失败，可能是空文件或损坏文件
                        return None
                    # 加载转换后的PNG
                    image = cv2.imread(str(cached_png))
                    if image is None:
                        return None
            else:
                # 使用OpenCV加载常规图像格式
                image = cv2.imread(str(img_file))
                if image is None:
                    return None
            
            if image is None or image.size == 0:
                self.logger.debug(f"图像为空或尺寸为0: {img_file.name}")
                return None
            
            # 质量检查
            quality_result = self.quality_filter.is_good_quality(image)
            if not quality_result:
                self.logger.debug(f"图像质量检查失败: {img_file.name} (可能对比度/亮度/清晰度不足)")
                return None
            
            # 预处理
            processed_image = self.preprocessor.preprocess_image(image)
            
            # 保存处理后的数据
            img_id = img_file.stem
            output_img_dir = self.output_dir / "cardiacnet" / img_id
            output_img_dir.mkdir(parents=True, exist_ok=True)
            
            # 保存原始图像
            cv2.imwrite(str(output_img_dir / "image.png"), image)
            
            # 保存处理后的图像
            np.save(str(output_img_dir / "processed_image.npy"), processed_image)
            
            return {
                'id': img_id,
                'type': 'cardiacnet',
                'image_path': str(output_img_dir / "image.png"),
                'processed_image_path': str(output_img_dir / "processed_image.npy"),
                'original_path': str(img_file)
            }
            
        except Exception as e:
            self.logger.error(f"处理CardiacNet图像失败 {img_file}: {e}", exc_info=True)
            return None
    
    def create_data_splits(self, all_samples: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
        """
        创建数据划分
        
        Args:
            all_samples: 所有样本
            
        Returns:
            划分后的数据
        """
        import random
        
        # 设置随机种子
        random.seed(self.config['data']['split']['random_seed'])
        
        # 打乱数据
        random.shuffle(all_samples)
        
        # 计算划分点
        total_samples = len(all_samples)
        train_end = int(total_samples * self.config['data']['split']['train_ratio'])
        val_end = train_end + int(total_samples * self.config['data']['split']['val_ratio'])
        
        # 划分数据
        splits = {
            'train': all_samples[:train_end],
            'val': all_samples[train_end:val_end],
            'test': all_samples[val_end:]
        }
        
        # 保存划分文件
        for split_name, samples in splits.items():
            split_file = self.output_dir / f"{split_name}_split.json"
            with open(split_file, 'w', encoding='utf-8') as f:
                json.dump({'samples': samples}, f, indent=2, ensure_ascii=False)
            
            self.logger.info(f"{split_name} 划分: {len(samples)} 个样本")
        
        return splits
    
    def process_camus2019(self, data_dir: Union[str, Path], max_patients: int = 100) -> Dict[str, Any]:
        """
        处理CAMUS2019数据集
        
        Args:
            data_dir: CAMUS2019数据目录
            max_patients: 最大处理患者数量（用于快速测试）
            
        Returns:
            处理结果统计
        """
        data_dir = Path(data_dir)
        self.logger.info(f"处理CAMUS2019数据集: {data_dir}")
        
        if not data_dir.exists():
            self.logger.error(f"CAMUS2019数据目录不存在: {data_dir}")
            return {}
        
        # 检查目录是否为空
        try:
            dir_contents = list(data_dir.iterdir())
            if not dir_contents:
                self.logger.warning(f"CAMUS2019数据目录为空: {data_dir}")
                return {
                    'dataset': 'CAMUS2019',
                    'total_samples': 0,
                    'valid_samples': 0,
                    'samples': []
                }
        except PermissionError:
            self.logger.error(f"无法访问CAMUS2019数据目录: {data_dir}（权限不足）")
            return {}
        
        # CAMUS2019结构: training/, testing/, 每个患者有2CH和4CH两个视角
        processed_samples = []
        total_samples = 0
        valid_samples = 0
        
        # 查找训练和测试目录
        train_dir = data_dir / 'training'
        test_dir = data_dir / 'testing'
        
        directories_to_process = []
        if train_dir.exists() and train_dir.is_dir():
            directories_to_process.append(('train', train_dir))
        if test_dir.exists() and test_dir.is_dir():
            directories_to_process.append(('test', test_dir))
        
        if not directories_to_process:
            # 如果没有标准目录结构，直接在根目录查找
            self.logger.warning("未找到标准CAMUS2019目录结构（training/testing），尝试在根目录查找")
            self.logger.info(f"根目录内容: {[d.name for d in dir_contents[:10]]}")
            directories_to_process = [('all', data_dir)]
        
        # 处理每个目录
        for split_name, split_dir in directories_to_process:
            # 查找所有患者目录（支持多种命名格式）
            # 1. 数字命名（标准格式）
            patient_dirs = [d for d in split_dir.iterdir() if d.is_dir() and d.name.isdigit()]
            
            # 2. 如果没找到，尝试其他命名格式
            if not patient_dirs:
                # 尝试以"patient"开头的目录
                patient_dirs = [d for d in split_dir.iterdir() if d.is_dir() and d.name.lower().startswith('patient')]
            
            # 3. 如果还没找到，尝试所有目录（排除已知的非患者目录）
            if not patient_dirs:
                exclude_dirs = {'training', 'testing', 'train', 'test', 'val', 'validation', 
                               'readme', 'license', 'docs', 'documentation', '.git', '__pycache__'}
                patient_dirs = [d for d in split_dir.iterdir() 
                              if d.is_dir() and d.name.lower() not in exclude_dirs]
            
            # 4. 如果仍然没找到，尝试递归查找包含.mhd或.mha文件的目录
            if not patient_dirs:
                self.logger.info("尝试递归查找包含医学图像文件的目录...")
                for subdir in split_dir.rglob('*'):
                    if subdir.is_dir():
                        # 检查目录中是否有.mhd或.mha文件
                        if list(subdir.glob('*.mhd')) or list(subdir.glob('*.mha')):
                            if subdir not in patient_dirs:
                                patient_dirs.append(subdir)
            
            if not patient_dirs:
                # 列出实际目录内容以便调试
                actual_dirs = [d.name for d in split_dir.iterdir() if d.is_dir()][:10]
                self.logger.warning(f"在 {split_dir} 中未找到患者目录")
                self.logger.warning(f"  实际目录内容: {actual_dirs}")
                continue
            
            self.logger.info(f"在 {split_dir} 中找到 {len(patient_dirs)} 个患者目录")
            if len(patient_dirs) <= 5:
                self.logger.debug(f"  患者目录: {[d.name for d in patient_dirs]}")
            
            # 限制处理数量
            if max_patients > 0:
                patient_dirs = patient_dirs[:max_patients]
                if split_name == 'train':
                    self.logger.info(f"快速模式：只处理前 {max_patients} 个患者")
            
            for patient_dir in tqdm(patient_dirs, desc=f"处理CAMUS2019 {split_name}"):
                try:
                    result = self._process_camus_patient(patient_dir, split_name)
                    if result:
                        processed_samples.extend(result)
                        valid_samples += len(result)
                    total_samples += 1
                except Exception as e:
                    self.logger.error(f"处理患者失败 {patient_dir}: {e}")
                    continue
        
        self.logger.info(f"CAMUS2019处理完成: {valid_samples}/{total_samples} 个样本")
        return {
            'dataset': 'CAMUS2019',
            'total_samples': total_samples,
            'valid_samples': valid_samples,
            'samples': processed_samples
        }
    
    def _process_camus_patient(self, patient_dir: Path, split: str) -> List[Dict[str, Any]]:
        """
        处理CAMUS2019单个患者
        
        Args:
            patient_dir: 患者目录
            split: 数据集划分 ('train' 或 'test')
            
        Returns:
            处理后的样本列表（每个视角一个样本）
        """
        samples = []
        patient_id = patient_dir.name
        
        # CAMUS2019通常包含2CH和4CH两个视角
        views = ['2CH', '4CH']
        
        for view in views:
            # 查找该视角的图像和掩码文件
            # 文件命名格式变体:
            # - patient{id}_{view}_{frame}.mhd/.mha
            # - {patient_id}_{view}_sequence.mhd/.mha
            # - {view}*.mhd/.mha
            # - *{view}*.mhd/.mha
            
            image_files = []
            
            # 1. 标准格式：patient{id}_{view}*
            patterns = [
                f"{patient_id}_{view}*.mhd",
                f"{patient_id}_{view}*.mha",
                f"patient{patient_id}_{view}*.mhd",
                f"patient{patient_id}_{view}*.mha",
                f"{patient_id}*{view}*.mhd",
                f"{patient_id}*{view}*.mha"
            ]
            for pattern in patterns:
                image_files.extend(patient_dir.glob(pattern))
            
            # 2. 如果没找到，尝试通用格式
            if not image_files:
                patterns = [
                    f"*{view}*.mhd",
                    f"*{view}*.mha",
                    f"*{view.lower()}*.mhd",
                    f"*{view.lower()}*.mha",
                    f"*{view.upper()}*.mhd",
                    f"*{view.upper()}*.mha"
                ]
                for pattern in patterns:
                    image_files.extend(patient_dir.glob(pattern))
            
            # 3. 如果还没找到，递归查找
            if not image_files:
                for pattern in [f"**/*{view}*.mhd", f"**/*{view}*.mha"]:
                    image_files.extend(patient_dir.glob(pattern))
            
            # 4. 如果仍然没找到，查找所有.mhd/.mha文件（可能是单视角数据）
            if not image_files:
                all_mhd = list(patient_dir.glob("*.mhd")) + list(patient_dir.glob("*.mha"))
                if all_mhd:
                    # 如果只有一个文件，可能是该视角的数据
                    if len(all_mhd) == 1:
                        image_files = all_mhd
                    else:
                        # 如果有多个文件，尝试根据文件名推断
                        for f in all_mhd:
                            if view.lower() in f.name.lower() or view.upper() in f.name.upper():
                                image_files.append(f)
            
            if not image_files:
                self.logger.debug(f"未找到 {view} 视角的图像文件: {patient_dir}")
                continue
            
            # 处理每个图像文件
            for img_file in image_files:
                try:
                    # 查找对应的掩码文件（支持多种命名格式）
                    mask_file = None
                    mask_patterns = [
                        # 标准格式
                        img_file.name.replace('_sequence', '_gt'),
                        img_file.name.replace('_sequence', '_label'),
                        img_file.name.replace('_sequence', '_mask'),
                        # 替换扩展名
                        img_file.name.replace('.mhd', '_gt.mhd'),
                        img_file.name.replace('.mha', '_gt.mha'),
                        img_file.name.replace('.mhd', '_label.mhd'),
                        img_file.name.replace('.mha', '_label.mha'),
                        # 添加_gt后缀
                        img_file.stem + '_gt' + img_file.suffix,
                        img_file.stem + '_label' + img_file.suffix,
                        img_file.stem + '_mask' + img_file.suffix,
                    ]
                    
                    for pattern in mask_patterns:
                        test_file = patient_dir / pattern
                        if test_file.exists():
                            mask_file = test_file
                            break
                    
                    # 如果还没找到，尝试递归查找
                    if not mask_file:
                        for pattern in ['**/*gt*.mhd', '**/*gt*.mha', '**/*label*.mhd', '**/*label*.mha']:
                            found = list(patient_dir.glob(pattern))
                            if found:
                                # 尝试匹配文件名
                                for f in found:
                                    if img_file.stem in f.stem or f.stem in img_file.stem:
                                        mask_file = f
                                        break
                            if mask_file:
                                break
                    
                    # 如果仍然没找到，记录警告但继续处理（某些数据可能没有掩码）
                    if not mask_file:
                        self.logger.debug(f"未找到掩码文件: {img_file.name}，将创建空掩码")
                    
                    # 读取图像（优先使用转换后的PNG，否则转换.mhd/.mha格式）
                    try:
                        # 创建转换缓存目录
                        cache_dir = self.output_dir / "cache" / "camus2019" / patient_id
                        cache_dir.mkdir(parents=True, exist_ok=True)
                        
                        # 检查是否有已转换的PNG文件
                        cached_png = cache_dir / f"{img_file.stem}.png"
                        if cached_png.exists():
                            # 直接加载PNG
                            image_bgr = cv2.imread(str(cached_png))
                            if image_bgr is not None:
                                # 转换为numpy数组格式
                                image_array = np.array([cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)])
                                frames = [image_array[0]]
                            else:
                                raise ValueError(f"无法加载缓存的PNG文件: {cached_png}")
                        else:
                            # 转换医学图像格式为PNG
                            if not self._convert_medical_image_to_png(img_file, cached_png):
                                continue
                            
                            # 加载转换后的PNG
                            image_bgr = cv2.imread(str(cached_png))
                            if image_bgr is None:
                                continue
                            image_array = np.array([cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)])
                            
                            # 尝试读取原始文件获取多帧信息
                            sitk = _import_sitk_silently()
                            if sitk is not None:
                                try:
                                    image = sitk.ReadImage(str(img_file))
                                    image_array_full = sitk.GetArrayFromImage(image)
                                    # 如果有多个帧，选择ED和ES帧
                                    if len(image_array_full.shape) == 3:
                                        ed_frame = image_array_full[len(image_array_full.shape[2] // 2)]
                                        es_frame = image_array_full[-1]
                                        # 转换这两帧为PNG并保存
                                        ed_png = cache_dir / f"{img_file.stem}_ed.png"
                                        es_png = cache_dir / f"{img_file.stem}_es.png"
                                        if not ed_png.exists():
                                            ed_frame_norm = ((ed_frame - ed_frame.min()) / 
                                                           (ed_frame.max() - ed_frame.min()) * 255).astype(np.uint8)
                                            cv2.imwrite(str(ed_png), cv2.cvtColor(ed_frame_norm, cv2.COLOR_GRAY2BGR))
                                        if not es_png.exists():
                                            es_frame_norm = ((es_frame - es_frame.min()) / 
                                                           (es_frame.max() - es_frame.min()) * 255).astype(np.uint8)
                                            cv2.imwrite(str(es_png), cv2.cvtColor(es_frame_norm, cv2.COLOR_GRAY2BGR))
                                        frames = [cv2.imread(str(ed_png), cv2.IMREAD_GRAYSCALE),
                                                 cv2.imread(str(es_png), cv2.IMREAD_GRAYSCALE)]
                                    else:
                                        frames = [image_array[0]]
                                except:
                                    frames = [image_array[0]]
                            else:
                                frames = [image_array[0]]
                        
                        # 读取掩码（如果存在）
                        masks = None
                        if mask_file and mask_file.exists():
                            try:
                                # 延迟导入SimpleITK
                                sitk = _import_sitk_silently()
                                if sitk is not None:
                                    mask = sitk.ReadImage(str(mask_file))
                                    mask_array = sitk.GetArrayFromImage(mask)
                                    if len(mask_array.shape) == 3:
                                        masks = [mask_array[len(mask_array) // 2], mask_array[-1]]
                                    else:
                                        masks = [mask_array]
                                else:
                                    masks = None
                            except Exception as e:
                                self.logger.warning(f"读取掩码文件失败 {mask_file}: {e}")
                                masks = None
                        
                        # 保存处理后的数据
                        sample_id = f"{patient_id}_{view}_{img_file.stem}"
                        output_sample_dir = self.output_dir / "camus2019" / sample_id
                        output_sample_dir.mkdir(parents=True, exist_ok=True)
                        
                        # 转换并保存帧
                        processed_frames = []
                        for i, frame in enumerate(frames):
                            # 归一化到0-255
                            if frame.max() > 255:
                                frame = ((frame - frame.min()) / (frame.max() - frame.min()) * 255).astype(np.uint8)
                            else:
                                frame = frame.astype(np.uint8)
                            
                            # 如果是2D，转换为3通道
                            if len(frame.shape) == 2:
                                frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
                            
                            # 调整尺寸
                            frame_resized = cv2.resize(frame, 
                                                      (self.config['data']['preprocessing']['image_size'][1],
                                                       self.config['data']['preprocessing']['image_size'][0]))
                            processed_frames.append(frame_resized)
                            cv2.imwrite(str(output_sample_dir / f"frame_{i:03d}.png"), frame_resized)
                        
                        # 保存掩码（如果存在）
                        if masks:
                            for i, mask in enumerate(masks):
                                if mask.max() > 1:
                                    mask = (mask > 0).astype(np.uint8) * 255
                                else:
                                    mask = mask.astype(np.uint8) * 255
                                cv2.imwrite(str(output_sample_dir / f"mask_{i:03d}.png"), mask)
                        
                        samples.append({
                            'id': sample_id,
                            'type': 'camus2019',
                            'patient_id': patient_id,
                            'view': view,
                            'frames_dir': str(output_sample_dir),
                            'num_frames': len(processed_frames),
                            'split': split
                        })
                        
                    except ImportError:
                        self.logger.warning("SimpleITK未安装，跳过.mhd/.mha文件")
                        continue
                    except Exception as e:
                        self.logger.error(f"读取图像失败 {img_file}: {e}")
                        continue
                        
                except Exception as e:
                    self.logger.error(f"处理图像失败 {img_file}: {e}")
                    continue
        
        return samples
    
    def run_preprocessing(self, 
                         brats_dir: Optional[Union[str, Path]] = None,
                         echonet_dir: Optional[Union[str, Path]] = None,
                         camus_dir: Optional[Union[str, Path]] = None,
                         cardiacnet_dir: Optional[Union[str, Path]] = None,
                         fast_mode: bool = True) -> Dict[str, Any]:
        """
        运行数据预处理（优化版）
        
        Args:
            brats_dir: BraTS2023数据目录
            echonet_dir: EchoNet-Dynamic数据目录
            camus_dir: CAMUS2019数据目录
            cardiacnet_dir: CardiacNet数据目录
            fast_mode: 快速模式（限制处理数量）
            
        Returns:
            处理结果
        """
        self.logger.info("开始数据预处理...")
        
        all_samples = []
        results = {}
        
        # 处理BraTS2023（已禁用，用户不再考虑此数据集）
        # if brats_dir and Path(brats_dir).exists():
        #     brats_result = self.process_brats2023(brats_dir)
        #     results['brats2023'] = brats_result
        #     all_samples.extend(brats_result.get('samples', []))
        
        # 处理EchoNet-Dynamic
        if echonet_dir and Path(echonet_dir).exists():
            # 检查是否已经全部处理完成
            echonet_processed_dir = self.output_dir / "echonet_dynamic"
            source_videos = len(list(Path(echonet_dir).glob("*.avi")))
            processed_videos = len([d for d in echonet_processed_dir.iterdir() if d.is_dir()]) if echonet_processed_dir.exists() else 0
            
            if processed_videos >= source_videos and source_videos > 0:
                self.logger.info(f"EchoNet-Dynamic已全部处理完成 ({processed_videos}/{source_videos})，跳过处理")
                # 从现有结果加载或创建空结果
                if 'echonet_dynamic' not in results:
                    results['echonet_dynamic'] = {
                        'dataset': 'EchoNet-Dynamic',
                        'total_samples': processed_videos,
                        'valid_samples': processed_videos,
                        'samples': [],
                        'skipped': True,
                        'reason': '已全部处理完成'
                    }
            else:
                max_videos = 50 if fast_mode else 0  # 快速模式只处理50个视频
                echonet_result = self.process_echonet_dynamic(echonet_dir, max_videos=max_videos)
                results['echonet_dynamic'] = echonet_result
                all_samples.extend(echonet_result.get('samples', []))
        
        # 处理CAMUS2019（需要SimpleITK，如果不可用则跳过）
        if camus_dir and Path(camus_dir).exists():
            # 检查SimpleITK是否可用
            sitk = _import_sitk_silently()
            if sitk is None:
                self.logger.warning("SimpleITK不可用，跳过CAMUS2019数据集（需要.mhd/.mha格式支持）")
                results['camus2019'] = {
                    'dataset': 'CAMUS2019',
                    'total_samples': 0,
                    'valid_samples': 0,
                    'samples': [],
                    'skipped': True,
                    'reason': 'SimpleITK不可用'
                }
            else:
                try:
                    max_patients = 50 if fast_mode else 0  # 快速模式只处理50个患者
                    camus_result = self.process_camus2019(camus_dir, max_patients=max_patients)
                    results['camus2019'] = camus_result
                    all_samples.extend(camus_result.get('samples', []))
                except Exception as e:
                    self.logger.error(f"处理CAMUS2019时出错: {e}，跳过该数据集")
                    results['camus2019'] = {
                        'dataset': 'CAMUS2019',
                        'total_samples': 0,
                        'valid_samples': 0,
                        'samples': [],
                        'skipped': True,
                        'reason': f'处理出错: {str(e)}'
                    }
        
        # 处理CardiacNet
        if cardiacnet_dir and Path(cardiacnet_dir).exists():
            max_images = 100 if fast_mode else 0  # 快速模式只处理100个图像
            cardiacnet_result = self.process_cardiacnet(cardiacnet_dir, max_images=max_images)
            results['cardiacnet'] = cardiacnet_result
            all_samples.extend(cardiacnet_result.get('samples', []))
        
        # 创建数据划分
        if all_samples:
            splits = self.create_data_splits(all_samples)
            results['splits'] = splits
        
        # 保存处理结果
        results_file = self.output_dir / "preprocessing_results.json"
        with open(results_file, 'w', encoding='utf-8') as f:
            json.dump(results, f, indent=2, ensure_ascii=False)
        
        self.logger.info(f"数据预处理完成，共处理 {len(all_samples)} 个样本")
        self.logger.info(f"结果保存到: {results_file}")
        
        return results


def main():
    """主函数"""
    # 在开始前提示用户关于ITK警告的信息
    print("="*60)
    print("注意：如果看到ITK版本警告，可以安全忽略")
    print("该警告不影响功能，脚本会继续正常运行")
    print("="*60)
    print()
    
    parser = argparse.ArgumentParser(description="超声心动图数据预处理（优化版）")
    parser.add_argument("--config", type=str, default="configs/training.yaml",
                       help="配置文件路径")
    parser.add_argument("--output_dir", type=str, default="data/processed/segmentation",
                       help="输出目录")
    parser.add_argument("--brats_dir", type=str, default="D:/Data/BraTS2023",
                       help="BraTS2023数据目录")
    parser.add_argument("--echonet_dir", type=str, default="D:/Data/EchoNet-Dynamic",
                       help="EchoNet-Dynamic数据目录")
    parser.add_argument("--camus_dir", type=str, default="D:/Data/CAMUS2019",
                       help="CAMUS2019数据目录（需要SimpleITK，如果不可用会自动跳过）")
    parser.add_argument("--cardiacnet_dir", type=str, default="D:/Data/CardiacNet",
                       help="CardiacNet数据目录")
    parser.add_argument("--skip_camus", action="store_true", default=False,
                       help="跳过CAMUS2019数据集（即使SimpleITK可用）")
    parser.add_argument("--skip_echonet", action="store_true", default=False,
                       help="跳过EchoNet-Dynamic数据集")
    parser.add_argument("--fast_mode", action="store_true", default=True,
                       help="快速模式（限制处理数量）")
    parser.add_argument("--full_mode", action="store_true", default=False,
                       help="完整模式（处理所有数据）")
    
    args = parser.parse_args()
    
    # 确定模式
    fast_mode = args.fast_mode and not args.full_mode
    
    # 创建预处理器
    preprocessor = EchoDataPreprocessor(
        config_path=args.config,
        output_dir=args.output_dir
    )
    
    # 运行预处理
    camus_dir = None if args.skip_camus else args.camus_dir
    echonet_dir = None if args.skip_echonet else args.echonet_dir
    results = preprocessor.run_preprocessing(
        brats_dir=args.brats_dir,
        echonet_dir=echonet_dir,
        camus_dir=camus_dir,
        cardiacnet_dir=args.cardiacnet_dir,
        fast_mode=fast_mode
    )
    
    # 打印结果摘要
    print("\n" + "="*60)
    print("数据预处理完成!")
    print("="*60)
    print(f"模式: {'快速模式' if fast_mode else '完整模式'}")
    
    for dataset, result in results.items():
        if isinstance(result, dict) and 'valid_samples' in result:
            print(f"{dataset}: {result['valid_samples']}/{result['total_samples']} 个样本")
    
    if 'splits' in results:
        print("\n数据划分:")
        for split_name, samples in results['splits'].items():
            print(f"  {split_name}: {len(samples)} 个样本")
    
    # 打印相似帧信息
    if 'echonet_dynamic' in results:
        echonet_samples = results['echonet_dynamic'].get('samples', [])
        if echonet_samples:
            total_similar_groups = sum(len(sample.get('similar_groups', [])) for sample in echonet_samples)
            print(f"\n相似帧检测:")
            print(f"  总相似帧组数: {total_similar_groups}")
            print(f"  平均每视频相似组数: {total_similar_groups / len(echonet_samples):.1f}")


if __name__ == "__main__":
    main()
